๐Ÿ”— Liste Dinamiche in C

Un Viaggio Completo nelle Strutture Dati Fondamentali
Dalle Basi alle Applicazioni Reali del Mondo Professionale

1. Introduzione: Il Problema degli Array

๐Ÿ“œ Una Storia di Limitazioni

Immaginate di gestire una playlist musicale. Con un array statico in C, decidete all'inizio: "Questa playlist avrร  massimo 100 canzoni". Cosa succede se:

  • L'utente vuole aggiungere la 101ยช canzone? โŒ Impossibile!
  • L'utente ha solo 10 canzoni? ๐Ÿ—‘๏ธ Spreco di memoria (90 slot vuoti)
  • Vuoi inserire una canzone in mezzo? ๐ŸŒ Devi spostare tutte le successive
  • Vuoi cancellare una canzone? ๐ŸŒ Devi compattare l'array

Nel 1955, quando i computer avevano pochissima memoria, i ricercatori si chiesero: "Esiste un modo migliore?" La risposta fu: Liste Concatenate (Linked Lists). Invece di allocare tutto in anticipo, allochi memoria on-demand, pezzo per pezzo, come una catena di anelli che cresce quando serve!

๐Ÿš‚ L'Analogia del Treno

Pensate a un treno. Ogni vagone รจ un nodo della lista:

  • Contenuto del vagone: I dati (es. numero, stringa, struttura)
  • Gancio al vagone successivo: Il puntatore next
  • Locomotiva: Il puntatore head (inizio della lista)

A differenza di un array (come un parcheggio con posti fissi uno accanto all'altro), i vagoni del treno possono essere ovunque in memoria! Ogni vagone "sa" dove trovare il prossimo grazie al suo gancio (puntatore).

Vuoi aggiungere un vagone? Lo attacchi dove serve!
Vuoi rimuovere un vagone? Lo sganci e ricolleghi i vagoni intorno!
Flessibilitร  totale! ๐ŸŽ‰

๐ŸŽฏ Perchรฉ Usare le Liste?

Caratteristica Array Statico Lista Concatenata
Dimensione โŒ Fissa (dichiarata a compile-time) โœ… Dinamica (cresce/si riduce)
Memoria โš ๏ธ Contigua (sprechi se usi poco) โœ… Sparsa (usa solo ciรฒ che serve)
Inserimento inizio โŒ O(n) - devi spostare tutto โœ… O(1) - cambi solo puntatori
Cancellazione inizio โŒ O(n) - compattazione โœ… O(1) - cambi puntatori
Accesso elemento i-esimo โœ… O(1) - accesso diretto โŒ O(n) - devi scorrere
Uso memoria extra โœ… Solo i dati โŒ Dati + puntatori

2. Concetti Fondamentali

Prima di scrivere codice, capiamo cosa รจ una lista concatenata e come funziona nella memoria del computer.

๐Ÿงฑ Il Nodo: Il Mattone Fondamentale

Una lista concatenata รจ fatta di nodi. Ogni nodo contiene:

  1. Dati: Il valore che vogliamo memorizzare (int, float, struct, ...)
  2. Puntatore next: L'indirizzo del nodo successivo (o NULL se รจ l'ultimo)
// Definizione di un nodo per lista di interi
typedef struct Nodo {
    int dato;                  // Il valore memorizzato
    struct Nodo *next;       // Puntatore al nodo successivo
} Nodo;

// Nota: Usiamo "struct Nodo *next" perchรฉ il tipo Nodo
// non รจ ancora completamente definito quando dichiariamo next
Visualizzazione di un Nodo in Memoria: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NODO โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ dato โ”‚ next โ”‚ โ† Struttura del nodo โ”‚ 42 โ”‚ 0x1234 โ”‚ โ† Valori esempio โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†‘ โ†‘ | | Valore Indirizzo del nodo successivo (o NULL se รจ l'ultimo)

๐Ÿ”— La Lista: Catena di Nodi

Una lista concatenata รจ una sequenza di nodi collegati tramite puntatori. Per gestirla, ci basta un puntatore head (testa) che punta al primo nodo.

Lista Semplicemente Concatenata (Singly Linked List): head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 1 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ Nodo 1 Nodo 2 Nodo 3 Nodo 4 โ€ข = puntatore al nodo successivo NULL = fine della lista

๐Ÿ’ก Cosa Succede in Memoria?

A differenza degli array, i nodi di una lista non sono contigui in memoria! Potrebbero essere sparsi ovunque:

Memoria RAM (semplificata): Indirizzo Contenuto โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 0x1000 [Altri dati...] 0x1004 [Altri dati...] 0x1008 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ† Nodo 3 โ”‚ 8 โ”‚ 0x1020 โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 0x1010 [Altri dati...] 0x1014 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ† Nodo 1 (head punta qui) โ”‚ 5 โ”‚ 0x1024 โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 0x1020 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ† Nodo 4 โ”‚ 1 โ”‚ NULL โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 0x1024 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ† Nodo 2 โ”‚ 3 โ”‚ 0x1008 โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 0x1028 [Altri dati...] Ogni nodo "sa" dove trovare il successivo grazie al puntatore!

3. Lista Semplicemente Concatenata

Iniziamo con la lista piรน semplice: ogni nodo punta solo al successivo. รˆ come una caccia al tesoro: ogni indizio ti porta al prossimo!

๐Ÿ“ฆ Struttura Completa

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Definizione del nodo
typedef struct Nodo {
    int dato;
    struct Nodo *next;
} Nodo;

// Definizione della lista (opzionale, ma utile)
typedef struct {
    Nodo *head;       // Puntatore al primo nodo
    int dimensione;   // Numero di nodi (opzionale)
} Lista;

// Inizializzazione lista vuota
Lista* crea_lista(void) {
    Lista *lista = (Lista*)malloc(sizeof(Lista));
    if (lista == NULL) {
        fprintf(stderr, "Errore: memoria insufficiente\n");
        return NULL;
    }
    lista->head = NULL;
    lista->dimensione = 0;
    return lista;
}

// Crea un nuovo nodo
Nodo* crea_nodo(int valore) {
    Nodo *nuovo = (Nodo*)malloc(sizeof(Nodo));
    if (nuovo == NULL) {
        fprintf(stderr, "Errore: impossibile creare nodo\n");
        return NULL;
    }
    nuovo->dato = valore;
    nuovo->next = NULL;
    return nuovo;
}

๐ŸŽฏ Perchรฉ Separare Lista e Nodo?

Potremmo usare solo Nodo *head senza la struttura Lista. Ma avere una struttura Lista ci permette di:

  • Tenere traccia della dimensione senza doverla ricalcolare
  • Aggiungere in futuro un puntatore tail (alla coda)
  • Avere metadati aggiuntivi (es. versione, stato)
  • Rendere il codice piรน organizzato e leggibile

4. Operazioni di Base

Ora implementiamo le operazioni fondamentali. Per ognuna, mostrerรฒ il codice, la visualizzazione e la spiegazione passo-passo.

โž• Inserimento in Testa (O(1))

Inserire all'inizio รจ velocissimo: basta creare un nuovo nodo e farlo puntare al vecchio head!

/**
 * @brief Inserisce un nuovo nodo all'inizio della lista
 * @param lista Puntatore alla lista
 * @param valore Valore da inserire
 * @return true se successo, false se errore
 * 
 * Complessitร : O(1) - Tempo costante!
 */
bool inserisci_in_testa(Lista *lista, int valore) {
    // 1. Crea nuovo nodo
    Nodo *nuovo = crea_nodo(valore);
    if (nuovo == NULL) {
        return false;
    }
    
    // 2. Il nuovo nodo punta al vecchio head
    nuovo->next = lista->head;
    
    // 3. Il nuovo nodo diventa il nuovo head
    lista->head = nuovo;
    
    // 4. Incrementa dimensione
    lista->dimensione++;
    
    return true;
}
Visualizzazione Passo-Passo: PRIMA: head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 1: Creiamo nuovo nodo con valore 10 โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 10 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 2: Nuovo nodo punta al vecchio head โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 10 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ” โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ”‚ โ†“ head โ”‚ โ†“ โ”‚ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”ดโ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 3: head punta al nuovo nodo head โ†“ โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 10 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ RISULTATO: Nuovo nodo in testa! โœ…

โž• Inserimento in Coda (O(n) senza tail)

Per inserire alla fine, dobbiamo scorrere tutta la lista per trovare l'ultimo nodo.

/**
 * @brief Inserisce un nuovo nodo alla fine della lista
 * @param lista Puntatore alla lista
 * @param valore Valore da inserire
 * @return true se successo, false se errore
 * 
 * Complessitร : O(n) - Dobbiamo scorrere tutta la lista
 */
bool inserisci_in_coda(Lista *lista, int valore) {
    // Crea nuovo nodo
    Nodo *nuovo = crea_nodo(valore);
    if (nuovo == NULL) {
        return false;
    }
    
    // Caso speciale: lista vuota
    if (lista->head == NULL) {
        lista->head = nuovo;
        lista->dimensione++;
        return true;
    }
    
    // Scorri fino all'ultimo nodo
    Nodo *corrente = lista->head;
    while (corrente->next != NULL) {
        corrente = corrente->next;  // Avanza
    }
    
    // corrente รจ ora l'ultimo nodo
    // Collegalo al nuovo nodo
    corrente->next = nuovo;
    lista->dimensione++;
    
    return true;
}

โšก Ottimizzazione: Puntatore alla Coda

Se aggiungiamo un campo Nodo *tail alla struttura Lista, possiamo inserire in coda in O(1)! Basta aggiornare tail ad ogni inserimento.

typedef struct {
    Nodo *head;
    Nodo *tail;  // โ† Nuovo campo!
    int dimensione;
} Lista;

// Inserimento in coda diventa O(1)!
bool inserisci_in_coda_veloce(Lista *lista, int valore) {
    Nodo *nuovo = crea_nodo(valore);
    if (nuovo == NULL) return false;
    
    if (lista->tail == NULL) {  // Lista vuota
        lista->head = lista->tail = nuovo;
    } else {
        lista->tail->next = nuovo;  // O(1)!
        lista->tail = nuovo;
    }
    
    lista->dimensione++;
    return true;
}

โž• Inserimento in Posizione

/**
 * @brief Inserisce un valore in una posizione specifica (0-indexed)
 * @param lista Puntatore alla lista
 * @param valore Valore da inserire
 * @param posizione Posizione dove inserire (0 = inizio)
 * @return true se successo, false se errore o posizione invalida
 * 
 * Complessitร : O(n)
 */
bool inserisci_in_posizione(Lista *lista, int valore, int posizione) {
    // Controllo validitร 
    if (posizione < 0 || posizione > lista->dimensione) {
        fprintf(stderr, "Posizione invalida\n");
        return false;
    }
    
    // Caso speciale: inserimento in testa
    if (posizione == 0) {
        return inserisci_in_testa(lista, valore);
    }
    
    // Crea nuovo nodo
    Nodo *nuovo = crea_nodo(valore);
    if (nuovo == NULL) return false;
    
    // Scorri fino al nodo PRIMA della posizione
    Nodo *corrente = lista->head;
    for (int i = 0; i < posizione - 1; i++) {
        corrente = corrente->next;
    }
    
    // Inserisci il nuovo nodo
    nuovo->next = corrente->next;  // Nuovo punta a quello dopo
    corrente->next = nuovo;         // Precedente punta a nuovo
    
    lista->dimensione++;
    return true;
}
Esempio: Inserisci 7 in posizione 2 PRIMA: head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ pos 0 pos 1 pos 2 Vogliamo inserire 7 in posizione 2 (tra 3 e 8) STEP 1: Scorriamo fino a pos 1 (il nodo 3) corrente โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 2: nuovo->next = corrente->next โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 7 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ” โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ”‚ โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 3: corrente->next = nuovo head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 7 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ FATTO! โœ…

๐Ÿ—‘๏ธ Cancellazione dalla Testa (O(1))

/**
 * @brief Rimuove e restituisce il primo elemento della lista
 * @param lista Puntatore alla lista
 * @param valore Puntatore dove salvare il valore rimosso (opzionale)
 * @return true se successo, false se lista vuota
 * 
 * Complessitร : O(1)
 */
bool rimuovi_testa(Lista *lista, int *valore) {
    if (lista->head == NULL) {
        fprintf(stderr, "Lista vuota\n");
        return false;
    }
    
    // Salva il nodo da rimuovere
    Nodo *da_rimuovere = lista->head;
    
    // Salva il valore (se richiesto)
    if (valore != NULL) {
        *valore = da_rimuovere->dato;
    }
    
    // Sposta head al nodo successivo
    lista->head = da_rimuovere->next;
    
    // Libera memoria del nodo rimosso
    free(da_rimuovere);
    
    lista->dimensione--;
    return true;
}

๐Ÿ—‘๏ธ Cancellazione per Valore

/**
 * @brief Rimuove la prima occorrenza di un valore
 * @param lista Puntatore alla lista
 * @param valore Valore da cercare e rimuovere
 * @return true se trovato e rimosso, false altrimenti
 * 
 * Complessitร : O(n)
 */
bool rimuovi_valore(Lista *lista, int valore) {
    if (lista->head == NULL) {
        return false;
    }
    
    // Caso speciale: il valore รจ in testa
    if (lista->head->dato == valore) {
        Nodo *temp = lista->head;
        lista->head = lista->head->next;
        free(temp);
        lista->dimensione--;
        return true;
    }
    
    // Cerca il valore
    Nodo *corrente = lista->head;
    while (corrente->next != NULL) {
        if (corrente->next->dato == valore) {
            // Trovato! Rimuovi il nodo successivo
            Nodo *da_rimuovere = corrente->next;
            corrente->next = da_rimuovere->next;  // Scavalca il nodo
            free(da_rimuovere);
            lista->dimensione--;
            return true;
        }
        corrente = corrente->next;
    }
    
    return false;  // Non trovato
}
Esempio: Rimuovi valore 3 PRIMA: head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ da_rimuovere STEP 1: Troviamo che corrente->next->dato == 3 corrente da_rimuovere โ†“ โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ STEP 2: corrente->next = da_rimuovere->next โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”ดโ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ค 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ†‘ (scollegato) STEP 3: free(da_rimuovere) head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ FATTO! โœ…

๐Ÿ” Ricerca

/**
 * @brief Cerca un valore nella lista
 * @param lista Puntatore alla lista
 * @param valore Valore da cercare
 * @return Puntatore al nodo se trovato, NULL altrimenti
 * 
 * Complessitร : O(n)
 */
Nodo* cerca(Lista *lista, int valore) {
    Nodo *corrente = lista->head;
    
    while (corrente != NULL) {
        if (corrente->dato == valore) {
            return corrente;  // Trovato!
        }
        corrente = corrente->next;
    }
    
    return NULL;  // Non trovato
}

/**
 * @brief Conta quante volte appare un valore
 */
int conta_occorrenze(Lista *lista, int valore) {
    int count = 0;
    Nodo *corrente = lista->head;
    
    while (corrente != NULL) {
        if (corrente->dato == valore) {
            count++;
        }
        corrente = corrente->next;
    }
    
    return count;
}

๐Ÿ–จ๏ธ Stampa e Visualizzazione

/**
 * @brief Stampa tutti gli elementi della lista
 */
void stampa_lista(Lista *lista) {
    if (lista->head == NULL) {
        printf("Lista vuota\n");
        return;
    }
    
    printf("Lista: ");
    Nodo *corrente = lista->head;
    
    while (corrente != NULL) {
        printf("%d", corrente->dato);
        
        if (corrente->next != NULL) {
            printf(" -> ");
        }
        
        corrente = corrente->next;
    }
    
    printf(" -> NULL\n");
}

/**
 * @brief Stampa con indirizzi di memoria (per debug)
 */
void stampa_lista_debug(Lista *lista) {
    printf("=== DEBUG LISTA ===\n");
    printf("Dimensione: %d\n", lista->dimensione);
    printf("Head: %p\n", (void*)lista->head);
    
    Nodo *corrente = lista->head;
    int i = 0;
    
    while (corrente != NULL) {
        printf("Nodo %d: [dato=%d, addr=%p, next=%p]\n",
               i++, corrente->dato, 
               (void*)corrente, 
               (void*)corrente->next);
        corrente = corrente->next;
    }
    printf("==================\n");
}

5. Lista Doppiamente Concatenata

Nelle liste doppie, ogni nodo ha due puntatori: uno al successivo (next) e uno al precedente (prev). Questo permette di scorrere la lista in entrambe le direzioni!

๐Ÿš‡ L'Analogia della Metropolitana

In una lista semplice, sei su un treno che va solo in una direzione. In una lista doppia, sei su una metropolitana: puoi andare avanti E indietro! Ogni stazione (nodo) sa quale รจ la prossima e quale รจ la precedente.

// Definizione nodo per lista doppia
typedef struct NodoDouble {
    int dato;
    struct NodoDouble *next;  // Puntatore al successivo
    struct NodoDouble *prev;  // Puntatore al precedente
} NodoDouble;

typedef struct {
    NodoDouble *head;
    NodoDouble *tail;  // Utile per inserimento in coda O(1)
    int dimensione;
} ListaDouble;
Lista Doppiamente Concatenata: head tail โ†“ โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 5 โ”‚NULLโ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚ โ€ขโ”€โ”€โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ†‘ โ†“ โ†‘ โ†“ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Ogni nodo ha: - dato: valore memorizzato - prev: puntatore al precedente (NULL per il primo) - next: puntatore al successivo (NULL per l'ultimo)

โž• Inserimento in Testa (Lista Doppia)

bool inserisci_in_testa_double(ListaDouble *lista, int valore) {
    NodoDouble *nuovo = (NodoDouble*)malloc(sizeof(NodoDouble));
    if (nuovo == NULL) return false;
    
    nuovo->dato = valore;
    nuovo->prev = NULL;
    nuovo->next = lista->head;
    
    if (lista->head != NULL) {
        lista->head->prev = nuovo;  // โ† Differenza chiave!
    } else {
        lista->tail = nuovo;  // Lista era vuota
    }
    
    lista->head = nuovo;
    lista->dimensione++;
    return true;
}

๐Ÿ”„ Vantaggi della Lista Doppia

6. Liste Circolari

In una lista circolare, l'ultimo nodo punta al primo invece che a NULL. รˆ come un girotondo: non c'รจ "fine"!

Lista Circolare: head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ†’โ”‚ 5 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 8 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ” โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ L'ultimo nodo punta al primo! Non c'รจ NULL.

๐ŸŽฏ Dove si Usano le Liste Circolari?

  • ๐ŸŽฎ Game Engine: Round-robin per turni di gioco
  • ๐Ÿ–ฅ๏ธ Sistema Operativo: Scheduling dei processi (ogni processo ha il suo turno ciclicamente)
  • ๐ŸŽต Media Player: Playlist in loop (quando finisce riprende dall'inizio)
  • ๐ŸŽฐ Buffer Circolare: Per streaming audio/video

7. Applicazioni nel Mondo Reale

Le liste concatenate non sono solo un esercizio accademico! Sono usate ovunque nel software professionale. Vediamo casi concreti.

๐ŸŒ 1. Browser Web - History e Tabs

Quando navighi su Internet, il browser tiene traccia delle pagine visitate usando una lista doppiamente concatenata:

  • Back (โ†): Vai al nodo precedente (prev)
  • Forward (โ†’): Vai al nodo successivo (next)
  • Nuova pagina: Inserimento dopo il nodo corrente (cancella tutto il "forward")
Browser History: โ†Back Forwardโ†’ โ†“ โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚Googleโ”‚NULLโ”‚ โ€ขโ”€โ”€โ”ผโ”€โ†’โ”‚Wikipediaโ”‚ โ€ขโ”€โ”€โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ†’โ”‚GitHub โ”‚ โ€ขโ”€โ”€โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ†“ โ†‘ โ†“ โ””โ”€โ”€โ”˜ (pagina corrente)

๐Ÿ’พ 2. Sistema Operativo - Process Scheduler

Il kernel di Linux usa liste circolari per gestire i processi! Ogni processo in attesa di CPU รจ in una lista circolare. Lo scheduler scorre la lista dando a ogni processo un time slice.

// Semplificazione di come Linux gestisce processi
struct task_struct {
    int pid;              // Process ID
    int priority;         // Prioritร 
    struct task_struct *next;
    struct task_struct *prev;
    // ... altri campi ...
};

// Lista circolare di processi pronti
struct task_struct *runqueue_head;

โœ๏ธ 3. Editor di Testo - Undo/Redo

Quando fai Ctrl+Z (undo) o Ctrl+Y (redo) in un editor, stai navigando una lista doppiamente concatenata di "stati" del documento!

"Hello" โ†’ "Hello World" โ†’ "Hello World!" โ†’ "Hello World! How are you?" โ†‘ (stato corrente) Ctrl+Z (undo) โ†’ vai a prev Ctrl+Y (redo) โ†’ vai a next

๐ŸŽต 4. Music Player - Playlist

Spotify, iTunes e altri player usano liste per gestire le playlist:

  • Lista semplice: Playlist normale (canzone dopo canzone)
  • Lista doppia: Playlist con "Previous" e "Next"
  • Lista circolare: Playlist in loop (repeat all)
  • Shuffle: Genera una lista randomizzata

๐Ÿ’พ 5. Memory Management - Free List

Il sistema di gestione memoria (malloc/free) usa internamente free lists - liste di blocchi di memoria libera! Quando chiami malloc(), il sistema:

  1. Cerca nella free list un blocco abbastanza grande
  2. Rimuove il blocco dalla lista (o lo splitta)
  3. Ti restituisce il puntatore

Quando chiami free(), il sistema:

  1. Inserisce il blocco nella free list
  2. Cerca di coalescere con blocchi adiacenti
// Semplificazione di come funziona malloc() internamente
typedef struct free_block {
    size_t size;              // Dimensione blocco
    struct free_block *next;  // Prossimo blocco libero
} free_block_t;

free_block_t *free_list_head;  // Lista di blocchi liberi

๐Ÿ—ƒ๏ธ 6. Database - Skip List

Redis (database in-memory popolarissimo) usa Skip Lists per implementare Sorted Sets. Una skip list รจ una lista multi-livello che permette ricerca veloce come un albero bilanciato, ma piรน semplice da implementare!

8. Gestione Avanzata della Memoria con Liste

Le liste dinamiche sono un campo minato per i memory leak! Vediamo i problemi comuni e come evitarli.

๐Ÿ’€ Problema 1: Perdere il Riferimento alla Testa

// โŒ ERRORE GRAVE - Memory leak
Nodo *head = crea_nodo(10);
head = crea_nodo(20);  // โŒ Perso il primo nodo!

// Il primo nodo (10) รจ ancora in memoria ma irraggiungibile
// โ†’ MEMORY LEAK!

๐Ÿ’€ Problema 2: Non Liberare Tutti i Nodi

// โŒ ERRORE - Libera solo il primo nodo
void distruggi_lista_sbagliato(Lista *lista) {
    free(lista->head);  // โŒ E gli altri nodi???
    free(lista);
}

// โœ… CORRETTO - Libera tutti i nodi
void distruggi_lista(Lista *lista) {
    Nodo *corrente = lista->head;
    
    while (corrente != NULL) {
        Nodo *temp = corrente;
        corrente = corrente->next;  // Salva next PRIMA di free!
        free(temp);
    }
    
    free(lista);
}

โš ๏ธ CRITICO: L'ordine conta!

// โŒ SBAGLIATO
while (corrente != NULL) {
    free(corrente);               // โ† Libera il nodo
    corrente = corrente->next;  // โŒ USE AFTER FREE!
                                 // stai accedendo a memoria liberata!
}

// โœ… CORRETTO
while (corrente != NULL) {
    Nodo *temp = corrente;         // โ† Salva puntatore
    corrente = corrente->next;  // โ† Avanza (PRIMA di free)
    free(temp);                   // โ† Ora รจ sicuro
}

๐Ÿ’€ Problema 3: Cicli nella Lista

Se crei accidentalmente un ciclo nella lista (un nodo che punta a un nodo precedente), la distruzione va in loop infinito!

/**
 * @brief Rileva se c'รจ un ciclo nella lista (Floyd's Algorithm)
 * @return true se c'รจ un ciclo, false altrimenti
 * 
 * Algoritmo della "Tartaruga e Lepre":
 * - Tartaruga: avanza di 1 nodo
 * - Lepre: avanza di 2 nodi
 * Se si incontrano โ†’ c'รจ un ciclo!
 */
bool ha_ciclo(Lista *lista) {
    if (lista->head == NULL) return false;
    
    Nodo *tartaruga = lista->head;
    Nodo *lepre = lista->head;
    
    while (lepre != NULL && lepre->next != NULL) {
        tartaruga = tartaruga->next;        // +1
        lepre = lepre->next->next;          // +2
        
        if (tartaruga == lepre) {
            return true;  // Si sono incontrate! Ciclo!
        }
    }
    
    return false;  // Nessun ciclo
}

9. Algoritmi Avanzati sulle Liste

๐Ÿ”„ Inversione di una Lista

Invertire una lista (es: 1โ†’2โ†’3 diventa 3โ†’2โ†’1) รจ un classico problema di colloqui!

/**
 * @brief Inverte una lista sul posto (in-place)
 * @param lista Puntatore alla lista da invertire
 * 
 * Complessitร : O(n) tempo, O(1) spazio
 */
void inverti_lista(Lista *lista) {
    Nodo *prev = NULL;
    Nodo *corrente = lista->head;
    Nodo *next = NULL;
    
    while (corrente != NULL) {
        // Salva il prossimo
        next = corrente->next;
        
        // Inverti il puntatore
        corrente->next = prev;
        
        // Avanza
        prev = corrente;
        corrente = next;
    }
    
    // prev รจ ora l'ultimo nodo (nuovo head)
    lista->head = prev;
}
Inversione Passo-Passo: INIZIALE: head โ†“ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 1 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 2 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 3 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ ITERAZIONE 1 (corrente = 1): prev=NULL corrente=1 next=2 Inverto: 1->next = NULL โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 1 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ ITERAZIONE 2 (corrente = 2): prev=1 corrente=2 next=3 Inverto: 2->next = 1 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 2 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 1 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ ITERAZIONE 3 (corrente = 3): prev=2 corrente=3 next=NULL Inverto: 3->next = 2 โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ 3 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 2 โ”‚ โ€ขโ”€โ”€โ”ผโ”€โ”€โ”€โ†’โ”‚ 1 โ”‚NULLโ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ†‘ new head FATTO! โœ…

๐Ÿ”— Merge di Due Liste Ordinate

/**
 * @brief Unisce due liste ordinate in una lista ordinata
 * @return Puntatore alla testa della lista unita
 * 
 * Complessitร : O(n + m)
 */
Nodo* merge_liste_ordinate(Nodo *l1, Nodo *l2) {
    // Caso base
    if (l1 == NULL) return l2;
    if (l2 == NULL) return l1;
    
    Nodo *risultato = NULL;
    
    // Scegli il piรน piccolo come head
    if (l1->dato <= l2->dato) {
        risultato = l1;
        risultato->next = merge_liste_ordinate(l1->next, l2);
    } else {
        risultato = l2;
        risultato->next = merge_liste_ordinate(l1, l2->next);
    }
    
    return risultato;
}

๐ŸŽฏ Trovare il Punto Medio (Fast & Slow Pointer)

/**
 * @brief Trova il nodo centrale della lista
 * Usa tecnica "fast & slow pointer"
 * 
 * Complessitร : O(n) con un solo passaggio!
 */
Nodo* trova_medio(Lista *lista) {
    if (lista->head == NULL) return NULL;
    
    Nodo *slow = lista->head;  // Avanza di 1
    Nodo *fast = lista->head;  // Avanza di 2
    
    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    // Quando fast arriva alla fine, slow รจ al centro!
    return slow;
}

10. Confronto con Altre Strutture Dati

Operazione Array Lista Semplice Lista Doppia Array Dinamico (Vector)
Accesso i-esimo โœ… O(1) โŒ O(n) โŒ O(n) โœ… O(1)
Inserimento inizio โŒ O(n) โœ… O(1) โœ… O(1) โŒ O(n)
Inserimento fine โŒ O(n) O(n) senza tail
โœ… O(1) con tail
โœ… O(1) โœ… O(1) ammortizzato
Rimozione inizio โŒ O(n) โœ… O(1) โœ… O(1) โŒ O(n)
Ricerca O(n) non ordinato
O(log n) ordinato
โŒ O(n) โŒ O(n) O(n) non ordinato
O(log n) ordinato
Memoria extra โœ… Nessuna โš ๏ธ 1 puntatore/nodo โŒ 2 puntatori/nodo โš ๏ธ Overhead realloc
Localitร  cache โœ… Ottima โŒ Pessima โŒ Pessima โœ… Buona

๐ŸŽฏ Quando Usare Cosa?

๐Ÿ“Š Usa Array quando:

  • Dimensione fissa e nota
  • Accesso casuale frequente
  • Performance critica (cache)
  • Poco inserimento/rimozione

๐Ÿ”— Usa Lista Semplice quando:

  • Dimensione variabile
  • Inserimento/rimozione in testa
  • Scorrimento unidirezionale
  • Stack, code, free lists

โ‡„ Usa Lista Doppia quando:

  • Serve scorrere avanti/indietro
  • Browser history, undo/redo
  • Deque (double-ended queue)
  • LRU cache

๐Ÿ“ˆ Usa Array Dinamico quando:

  • Dimensione variabile
  • Accesso casuale necessario
  • Inserimenti principalmente in coda
  • Meglio performance cache

11. Visualizzatore Interattivo di Liste

๐ŸŽฎ Gioca con le Liste!

Usa i controlli qui sotto per creare, modificare e visualizzare una lista dinamica in tempo reale.

Lista vuota. Aggiungi alcuni nodi per iniziare!
Statistiche:
Nodi: 0 | Memoria: 0 bytes | Primo: - | Ultimo: -

12. Quiz Interattivi - Metti alla Prova le Tue Conoscenze!

๐Ÿ“ Quiz 1: Qual รจ la complessitร  temporale per inserire un elemento in TESTA di una lista concatenata?
  • A) O(1) - Costante
  • B) O(n) - Lineare
  • C) O(log n) - Logaritmica
  • D) O(nยฒ) - Quadratica
๐Ÿ“ Quiz 2: Quale vantaggio principale hanno le liste concatenate rispetto agli array statici?
  • A) Accesso piรน veloce agli elementi
  • B) Dimensione dinamica (cresce/diminuisce)
  • C) Usano meno memoria
  • D) Migliore cache locality
๐Ÿ“ Quiz 3: In una lista doppiamente concatenata, ogni nodo contiene:
  • A) Solo dato e puntatore next
  • B) Dato, puntatore next e puntatore prev
  • C) Solo dato e puntatore prev
  • D) Dato e due puntatori next
๐Ÿ“ Quiz 4: Cosa succede se dimentichi di fare free() di tutti i nodi di una lista?
  • A) Nulla, il sistema libera automaticamente
  • B) Memory leak - memoria sprecata
  • C) Crash immediato del programma
  • D) I nodi vengono riutilizzati
๐Ÿ“ Quiz 5: Qual รจ l'errore in questo codice di distruzione?
while (current != NULL) {
    free(current);
    current = current->next;
}
  • A) Dovrebbe essere current->prev invece di next
  • B) Use after free - accedi a current dopo free()
  • C) Manca il check di NULL
  • D) Non c'รจ nessun errore
๐Ÿ“ Quiz 6: In una lista circolare, l'ultimo nodo punta a:
  • A) NULL
  • B) Il primo nodo (head)
  • C) Se stesso
  • D) Il penultimo nodo
๐Ÿ“ Quiz 7: Quale applicazione reale NON usa tipicamente liste concatenate?
  • A) Browser history (avanti/indietro)
  • B) Music player playlist
  • C) Ricerca binaria in array ordinato
  • D) Undo/Redo in editor di testo
๐Ÿ“ Quiz 8: Quale tecnica si usa per rilevare cicli in una lista?
  • A) Floyd's Algorithm (tartaruga e lepre)
  • B) Binary Search
  • C) Bubble Sort
  • D) Quick Sort

๐ŸŽ“ Recap Finale: Le Regole d'Oro delle Liste

  1. Ogni malloc() di nodo deve avere una free() - Nessuna eccezione!
  2. Salva next PRIMA di fare free() - Altrimenti use-after-free
  3. Inizializza sempre i puntatori - next = NULL per l'ultimo nodo
  4. Gestisci il caso lista vuota - Controlla se head == NULL
  5. Usa una funzione distruggi_lista - Libera tutti i nodi in loop
  6. Testa cicli con Floyd's Algorithm - Prima di operazioni pericolose
  7. Scegli il tipo giusto - Semplice vs Doppia vs Circolare
  8. Disegna sempre su carta prima - Visualizza l'algoritmo
  9. Usa Valgrind per memory leak - Trova quei free() dimenticati
  10. Pratica, pratica, pratica - Le liste diventano naturali con l'esperienza!

๐ŸŽฏ Conclusione: La Bellezza delle Liste

Le liste concatenate sono come i mattoncini LEGO delle strutture dati. Semplici da capire concettualmente, ma incredibilmente versatili! Con esse puoi costruire:

  • Stack e Code
  • Grafi (liste di adiacenza)
  • Hash table con chaining
  • Memory allocator
  • E molto altro!

Ogni grande programmatore ha passato ore a debuggare liste. รˆ normale! Ogni segmentation fault ti insegna qualcosa. Ogni memory leak ti rende piรน attento. Ogni algoritmo che implementi ti avvicina alla padronanza.

Ricorda: non stai solo manipolando puntatori. Stai orchestrando la memoria del computer come un direttore d'orchestra! ๐ŸŽญโœจ

๐Ÿ“š Risorse per Approfondire

  • Visualizzatori Online:
    • VisuAlgo - Visualizzazione animata di algoritmi
    • Data Structure Visualizations - USF
  • Pratica:
    • LeetCode - Sezione "Linked List" (oltre 100 problemi!)
    • HackerRank - Data Structures track
    • Codewars - Kata sulle liste
  • Libri:
    • "Introduction to Algorithms" (CLRS) - Capitolo sulle liste
    • "The Algorithm Design Manual" - Skiena
  • Codice Reale:
    • Linux Kernel - list.h (liste doppiamente concatenate)
    • Redis - adlist.c (implementazione ottimizzata)